﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace InstantSpine.Core.ParaSpine
{
    /// <summary>
    /// Manages interop with the original APDL-Impl of ParaSpine
    /// </summary>
    public sealed class ApdlParaSpine : ISpineFEModelGenerator
    {
        /// <summary>
        /// No-op implementation of the status callback
        /// </summary>
        static readonly Action<ModelGenerationPhase, int, int> nop = ((_1, _2, _3) => { });

        /// <summary>
        /// Default name of the main ParaSpine script file
        /// </summary>
        const string defaultParaSpineScript = "para-spine.inp";

        /// <summary>
        /// Default name of the output/log file generated by ANSYS
        /// </summary>
        const string defaultStandardOutputFile = "para-spine.out";

        /// <summary>
        /// Default name of the generated parameter spec macro file
        /// </summary>
        const string defaultConfigScriptName = "override-params.mac";

        /// <summary>
        /// Default name of the file generated by ParaSpine (Abaqus input deck)
        /// </summary>
        const string generatedModelFile = "abaqmesh.inp";
        
        /// <summary>
        /// ANSYS product string, e.g. 'aa_t_a'
        /// </summary>
        string AnsysProductString { get; set; }

        /// <summary>
        /// Full path to ANSYS executable (including exe file)
        /// </summary>
        string PathToAnsys { get; set; }

        /// <summary>
        /// Full path to the folder containing the ParaSpine macros
        /// </summary>
        string PathToParaSpine { get; set; }

        public ApdlParaSpine(string ansysProductString, string pathToAnsys, string pathToParaSpine) {
            this.AnsysProductString = ansysProductString;
            this.PathToAnsys = pathToAnsys;
            this.PathToParaSpine = pathToParaSpine;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="modelSetup"></param>
        /// <param name="destination"></param>
        /// <param name="setStatus"></param>
        /// <exception cref="System.ArgumentException"/>
        /// <exception cref="InstantSpine.Core.ParaSpineNotFoundException"/>
        /// <exception cref="InstantSpine.Core.ParaSpineNotWriteableException"/>
        /// <exception cref="InstantSpine.Core.ParaSpineStartupScriptNotFoundException"/>
        /// <exception cref="InstantSpine.Core.AnsysNotFoundException"/>
        /// <exception cref="InstantSpine.Core.AnsysAbnormalExitException"/>
        /// <exception cref="InstantSpine.Core.InvalidAnsysProductStringException"/>
        /// <exception cref="InstantSpine.Core.ParaSpineModelGenerationException"/>
        public void GenerateFEModel(ISpineModel modelSetup, string destination, Action<ModelGenerationPhase, int, int> setStatus = null) {
            if (modelSetup == null)
                throw new ArgumentException("modelSetup must not be null.");

            if (string.IsNullOrEmpty(destination))
                throw new ArgumentException("destination must not be null or empty.");
            
            // use no-op action, if no call-back was supplied
            setStatus = setStatus ?? nop;

            // ANSYS' APDL interpreter expects en-US locale
            var originalCulture = System.Threading.Thread.CurrentThread.CurrentCulture;
            System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture;

            // Generate APDL macro which provides all the parameter settings
            // and save it to a file called defaultConfigScriptName in PathToAnsys
            setStatus(ModelGenerationPhase.ExportingParameterization, 1, 3);
            GenerateApdlConfigFile(modelSetup);
            System.Threading.Thread.Sleep(300);
            
            // run ParaSpine with the injected config file by calling ANSYS
            // (actual model/mesh generation phase)
            setStatus(ModelGenerationPhase.GeneratingFEModel, 2, 3);
            //CallPreprocessor();
            System.Threading.Thread.Sleep(300);
            
            setStatus(ModelGenerationPhase.ConvertingToTargetFormat, 3, 3);
            ConvertToTargetSolverFormat();
            System.Threading.Thread.Sleep(300);

            // reset to original culture
            System.Threading.Thread.CurrentThread.CurrentCulture = originalCulture;
        }

        private void GenerateApdlConfigFile(ISpineModel modelSetup) {
            var config = new APDLScript();

            config.Comment("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
            config.Comment("!! GENERAL MODEL SETUP");
            config.Comment("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
            config.EmptyLine();

            var options = modelSetup.Options;

            config.Assign("radialLineDivsInner", options.InnerRadialSubdivisions.ParameterValue);
            config.Assign("radialLineDivs", options.OuterRadialSubdivisions.ParameterValue);
            config.Assign("circumferentialDivs", options.CircumferentialSubdivisions.ParameterValue);
            config.Assign("cortShellDivisions", options.CorticalShellRadialSubdivisions.ParameterValue);
            config.Assign("cartLayerDivisions", options.EndplateAxialSubdivisions.ParameterValue);
            config.Assign("axialElDivsVert", options.AxialSubdivisionsVB.ParameterValue);
            config.Assign("axialElDivsDisc", options.AxialSubdivisionsDisc.ParameterValue);
            config.Assign("meshVertFrom", options.FirstVB.ParameterValue);
            config.Assign("meshVertTo", options.LastVB.ParameterValue);
            config.EmptyLine();
            
            config.Comment("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
            config.Comment("!! ANATOMY PARAMETERS");
            config.Comment("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
            config.EmptyLine();
            
            foreach (var anatomy in modelSetup.Anatomy.VertebraeAndDiscs) {
                var apdlVbId = (anatomy.VertebraID + 1).ToString();
                config.Comment(string.Format(" Anatomy paramters for {0}", (VertebraIDs)anatomy.VertebraID));
                config.Assign("vertebral_body_height_t"    + apdlVbId, anatomy.VertebralBodyHeightPosterior);
                config.Assign("end_plate_width_superior_t" + apdlVbId, anatomy.EndplateWidthSuperior);
                config.Assign("end_plate_width_inferior_t" + apdlVbId, anatomy.EndplateWidthInferior);
                config.Assign("end_plate_depth_superior_t" + apdlVbId, anatomy.EndplateDepthSuperior);
                config.Assign("end_plate_depth_inferior_t" + apdlVbId, anatomy.EndplateDepthInferior);
                config.Assign("disc_height_t"              + apdlVbId, anatomy.DiscHeight);
                config.Assign("pedicle_length_tho_t"       + apdlVbId, anatomy.PedicleLength);
                config.Assign("pedicle_inc_sag_tho_r_t"    + apdlVbId, anatomy.PedicleSagitalInclinationRight);
                config.Assign("pedicle_inc_sag_tho_l_t"    + apdlVbId, anatomy.PedicleSagitalInclinationLeft);
                config.Assign("pedicle_inc_trans_tho_r_t"  + apdlVbId, anatomy.PedicleTransverseInclinationRight);
                config.Assign("pedicle_inc_trans_tho_l_t"  + apdlVbId, anatomy.PedicleTransverseInclinationLeft);
                config.Assign("facet_width_tho_sup_r_t"    + apdlVbId, anatomy.FacetWidthRight);
                config.Assign("facet_width_tho_sup_l_t"    + apdlVbId, anatomy.FacetWidthLeft);
                config.Assign("facet_height_tho_sup_r_t"   + apdlVbId, anatomy.FacetHeightRight);
                config.Assign("facet_height_tho_sup_l_t"   + apdlVbId, anatomy.FacetHeightLeft);
                config.Assign("FACET_CAX_tho_sup_r_t"      + apdlVbId, anatomy.FacetTiltRightX);
                config.Assign("FACET_CAX_tho_sup_l_t"      + apdlVbId, anatomy.FacetTiltLeftX);
                config.Assign("FACET_CAY_tho_sup_r_t"      + apdlVbId, anatomy.FacetTiltRightY);
                config.Assign("FACET_CAY_tho_sup_l_t"      + apdlVbId, anatomy.FacetTiltLeftY);
                config.EmptyLine();
            }

            config.Comment("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
            config.Comment("!! MATERIAL PROPERTIES");
            config.Comment("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
            config.EmptyLine();

            var materials = modelSetup.Materials;

            config.Comment(" Facet joints' contact surfaces");
            if (materials.FacetsInnerLayer.YoungsModulus.HasValue)
                config.Assign("mpFacetsEmod", materials.FacetsInnerLayer.YoungsModulus);
            if (materials.FacetsInnerLayer.PoissonsRatio.HasValue)
                config.Assign("mpFacetsNu", materials.FacetsInnerLayer.PoissonsRatio);
            config.EmptyLine();            

            config.Comment(" Cortex");
            if (materials.Cortex.YoungsModulus.HasValue)
                config.Assign("mpCortexEmod", materials.Cortex.YoungsModulus);
            if (materials.Cortex.PoissonsRatio.HasValue)
                config.Assign("mpCortexNu", materials.Cortex.PoissonsRatio);
            config.EmptyLine();

            config.Comment(" Spongiosa");
            if (materials.Spongiosa.YoungsModulus.HasValue)
                config.Assign("mpSpongiosaEmod", materials.Spongiosa.YoungsModulus);
            if (materials.Spongiosa.PoissonsRatio.HasValue)
                config.Assign("mpSpongiosaNu", materials.Spongiosa.PoissonsRatio);
            config.EmptyLine();

            config.Comment(" Cartilaginous endplate (above nucleus)");
            if (materials.CartilaginousEndplate.YoungsModulus.HasValue)
                config.Assign("mpCartEPEmod", materials.CartilaginousEndplate.YoungsModulus);
            if (materials.CartilaginousEndplate.PoissonsRatio.HasValue)
                config.Assign("mpCartEPNu", materials.CartilaginousEndplate.PoissonsRatio);
            config.EmptyLine();

            config.Comment(" Cartilaginous endplate (above annulus)");
            if (materials.OuterCartilaginousEndplate.YoungsModulus.HasValue)
                config.Assign("mpCartEPAnnEmod", materials.OuterCartilaginousEndplate.YoungsModulus);
            if (materials.OuterCartilaginousEndplate.PoissonsRatio.HasValue)
                config.Assign("mpCartEPAnnNu", materials.OuterCartilaginousEndplate.PoissonsRatio);
            config.EmptyLine();

            config.Comment(" Follower load anchor");
            if (materials.FollowerLoadAnchor.YoungsModulus.HasValue)
                config.Assign("mpFollowerAnchorEMod", materials.FollowerLoadAnchor.YoungsModulus);
            if (materials.FollowerLoadAnchor.PoissonsRatio.HasValue)
                config.Assign("mpFollowerAnchorNu", materials.FollowerLoadAnchor.PoissonsRatio);
            config.EmptyLine();

            config.Comment(" Annulus ground substance");
            if (materials.AnnulusGroundSubstance.ApparentYoungsModulus.HasValue)
                config.Assign("mpAnnulusMedEmod", materials.AnnulusGroundSubstance.ApparentYoungsModulus);
            if (materials.AnnulusGroundSubstance.ApparentPoissonsRatio.HasValue)
                config.Assign("mpAnnulusMedNu", materials.AnnulusGroundSubstance.ApparentPoissonsRatio);
            config.EmptyLine();

            config.Comment(" Annulus ground substance (anterior)");
            if (materials.AnnulusGroundSubstanceAnterior.ApparentYoungsModulus.HasValue)
                config.Assign("mpAnnulusAntEmod", materials.AnnulusGroundSubstanceAnterior.ApparentYoungsModulus);
            if (materials.AnnulusGroundSubstanceAnterior.ApparentPoissonsRatio.HasValue)
                config.Assign("mpAnnulusAntNu", materials.AnnulusGroundSubstanceAnterior.ApparentPoissonsRatio);
            config.EmptyLine();

            config.Comment(" Annulus ground substance (posterior)");
            if (materials.AnnulusGroundSubstancePosterior.ApparentYoungsModulus.HasValue)
                config.Assign("mpAnnulusPostEmod", materials.AnnulusGroundSubstancePosterior.ApparentYoungsModulus);
            if (materials.AnnulusGroundSubstancePosterior.ApparentPoissonsRatio.HasValue)
                config.Assign("mpAnnulusPostNu", materials.AnnulusGroundSubstancePosterior.ApparentPoissonsRatio);
            config.EmptyLine();
            
            config.Comment(" Nucleus");
            if (materials.Nucleus.BulkModulus.HasValue)
                config.Assign("mpNucleusBulk", materials.Nucleus.BulkModulus);
            if (materials.Nucleus.Density.HasValue)
                config.Assign("mpNucleusDensity", materials.Nucleus.Density);
            config.EmptyLine();

            int layerNo = 0;
            foreach (var layer in materials.FiberLayers.Layers) {
                ++layerNo;
                config.Comment(string.Format(" Annulus fibers, {0}", layer.Name));
                var arrayName = "mpAnnulusFiberLayer" + layerNo;
                config.MaterialTestdataToArray(layer.FiberMaterialProperties, arrayName);
                config.EmptyLine();                
            }

            config.Comment(" Fiber angles, anterior");            
            config.DefineArray("antAngles", 8);
            config.Assign("antAngles(1)", materials.FiberLayers.Layers
                .Select(l => l.AnteriorFiberAngle.ToString())
                .Aggregate((s1, s2) => s1 + ", " + s2));
            config.Comment(" Fiber angles, posterior");
            config.DefineArray("postAngles", 8);
            config.Assign("postAngles(1)", materials.FiberLayers.Layers
                .Select(l => l.PosteriorFiberAngle.ToString())
                .Aggregate((s1, s2) => s1 + ", " + s2));
            config.EmptyLine();

            config.Comment(" SSL");            
            config.MaterialTestdataToArray(materials.SSL, "mpSSL");
            config.EmptyLine();

            config.Comment(" ALL");
            config.MaterialTestdataToArray(materials.ALL, "mpALL34");
            config.EmptyLine();

            config.Comment(" PLL");
            config.MaterialTestdataToArray(materials.PLL, "mpPLL");
            config.EmptyLine();

            config.Comment(" LF");
            config.MaterialTestdataToArray(materials.LF, "mpLF");
            config.EmptyLine();

            config.Comment(" ISL");
            config.MaterialTestdataToArray(materials.ISL, "mpISL");
            config.EmptyLine();

            config.Comment(" ITL");
            config.MaterialTestdataToArray(materials.ITL, "mpITL");
            config.EmptyLine();

            config.Comment(" CL");
            config.MaterialTestdataToArray(materials.CL, "mpCL");
            config.EmptyLine();

            if (!Directory.Exists(PathToParaSpine))
                throw new ParaSpineNotFoundException(PathToParaSpine);

            try {
                File.WriteAllText(Path.Combine(PathToParaSpine, defaultConfigScriptName), config.ScriptSource);
            }
            catch (UnauthorizedAccessException uaax) {
                throw new ParaSpineNotWriteableException(uaax);
            }
        }

        private void CallPreprocessor() {
            if (!Directory.Exists(PathToParaSpine))
                throw new ParaSpineNotFoundException(PathToParaSpine);

            if (!File.Exists(Path.Combine(PathToParaSpine, defaultParaSpineScript)))
                throw new ParaSpineStartupScriptNotFoundException(defaultParaSpineScript);

            if (!File.Exists(PathToAnsys))
                throw new AnsysNotFoundException(PathToAnsys);

            if (string.IsNullOrEmpty(AnsysProductString))
                throw new InvalidAnsysProductStringException();

            var ansysStartInfo = new ProcessStartInfo {
                FileName = PathToAnsys,
                Arguments = string.Format("-p {0} -b -dir {1} -i {2} -o {3}", 
                    AnsysProductString, 
                    PathToParaSpine, 
                    Path.Combine(PathToParaSpine, defaultParaSpineScript), 
                    Path.Combine(PathToParaSpine, defaultStandardOutputFile)),
                CreateNoWindow = true,
                UseShellExecute = false
            };

            var ansysProc = Process.Start(ansysStartInfo);            
            ansysProc.WaitForExit();

            var decodedExitCode = AnsysAbnormalExitException.DecodeAnsysExitCode(ansysProc.ExitCode);
            if (decodedExitCode != AnsysExitConditions.NormalExit &&
                decodedExitCode != AnsysExitConditions.EndOfRun)
                throw new AnsysAbnormalExitException(ansysProc.ExitCode);

            if (!File.Exists(Path.Combine(PathToParaSpine, generatedModelFile)))
                throw new ParaSpineModelGenerationException(string.Format("Cannot find model file {0}", generatedModelFile));
        }

        private void ConvertToTargetSolverFormat() {
            // For now: simple switch on TargetFESolver
            // later: encapsulate behavior
            //
            // Abaqus: move abaqmesh.inp to target file/location
            // FEBio: call converter lib on abaqmesh.inp
        }
    }
}
